#include "JSIExecutor.h"

#include <assert.h>

#include <cmath>
#include <limits>
#include <sstream>

#include "utils/DynamicConverter.h"
#include "utils/LogInfo.h"
#include "utils/Logger.h"
#include "utils/Trace.h"

namespace jsiexecutor {
extern const std::string NATIVE_MODULE_SCRIPT;
extern const char* GLOBAL_THIS;

class JSIExecutor::NativeModuleProxy : public HostObject {
 public:
  NativeModuleProxy(std::shared_ptr<JSINativeModules> nativeModules) : weakNativeModules_(nativeModules) {}

  Value get(Runtime& rt, const PropNameID& name) override {
    if (name.utf8(rt) == "name") {
      return String::createFromAscii(rt, "NativeModules");
    }

    auto nativeModules = weakNativeModules_.lock();
    if (!nativeModules) {
      logError("no native module found\n");
      return nullptr;
    }

    return nativeModules->getModule(rt, name);
  }

  void set(Runtime&, const PropNameID&, const Value&) override {
    throw std::runtime_error("Unable to do set on NativeModules");
  }

 private:
  std::weak_ptr<JSINativeModules> weakNativeModules_;
};

JSIExecutor::JSIExecutor(std::shared_ptr<Runtime> runtime, std::shared_ptr<ModuleRegistry> moduleRegistry)
    : runtime_(runtime),
      globalObj_(runtime->global()),
      moduleRegistry_(moduleRegistry),
      nativeModules_(std::make_shared<JSINativeModules>(moduleRegistry)),
      callbackId_(0) {}

void JSIExecutor::bindNativeLogger(Logger logger) {
  assert(globalObj_.hasProperty(*runtime_, "console"));
  Object object = globalObj_.getPropertyAsObject(*runtime_, "console");
  object.setProperty(
      *runtime_, "log",
      Function::createFromHostFunction(*runtime_, PropNameID::forUtf8(*runtime_, "log"), 1,
                                       [logger](facebook::jsi::Runtime& runtime, const facebook::jsi::Value&,
                                                const facebook::jsi::Value* args, size_t count) {
                                         std::stringstream stream;
                                         for (int i = 0; i < count; ++i) {
                                           std::string str;
                                           if (args[i].isString()) {
                                             str = args[i].getString(runtime).utf8(runtime);
                                           } else if (args[i].isNumber()) {
                                             if (args[i].getNumber() == std::floor(args[i].getNumber())) {
                                               str = std::to_string((int)args[i].getNumber());
                                             } else {
                                               str = std::to_string(args[i].getNumber());
                                             }
                                           } else {
                                             str = args[i].toString(runtime).utf8(runtime);
                                           }
                                           stream << str << " ";
                                         }
                                         logger(stream.str().c_str());
                                         logger("\n");
                                         return facebook::jsi::Value::undefined();
                                       }));
}

void JSIExecutor::initializeRuntime() {
  Trace s("JSIExecutor::initializeRuntime");
  setGlobalHostFunc(
      "invoke",
      [this](Runtime& runtime, const Value&, const Value* args, size_t count) {
        assert(count == 3);
        auto module = args[0].toString(*runtime_).utf8(*runtime_);
        auto method = args[1].toString(*runtime_).utf8(*runtime_);
        // the 3rd param should always be an array and contains all params and callbacks
        assert(args[2].getObject(*runtime_).isArray(*runtime_));
        auto array = args[2].getObject(*runtime_).getArray(*runtime_);
        return this->callNativeModule(module, method, std::move(array));
      },
      3);
  runtime_->global().setProperty(
      *runtime_, "NativeModules",
      Object::createFromHostObject(*runtime_, std::make_shared<NativeModuleProxy>(nativeModules_)));

  // a better way to do this instead of doing it in js?
  loadBundle(std::make_unique<facebook::jsi::StringBuffer>(NATIVE_MODULE_SCRIPT), "nativeModule");
}

void JSIExecutor::loadBundle(const std::shared_ptr<const Buffer>& buffer, const std::string& sourceUrl) {
  Trace s("JSIExecutor::loadBundle - bundle: " + sourceUrl);
  runtime_->evaluateJavaScript(buffer, sourceUrl);
}

Value JSIExecutor::callFunctionVec(int expressionIdx, const std::vector<Value>& args) {
  Trace s("JSIExecutor::callFunctionVec - expressionIdx: " + std::to_string(expressionIdx));
  Object globalThis = globalObj_.getPropertyAsObject(*runtime_, GLOBAL_THIS);
  Object expression = globalThis.getPropertyAsObject(*runtime_, EXPRESSION);
  Function func = expression.getPropertyAsFunction(*runtime_, std::to_string(expressionIdx).c_str());
  return func.call(*runtime_, args.data(), args.size());
}

Value JSIExecutor::callFunction(int expressionIdx, const std::initializer_list<Value>& args) {
  Trace s("JSIExecutor::callFunction - expressionIdx: " + std::to_string(expressionIdx));
  Object globalThis = globalObj_.getPropertyAsObject(*runtime_, GLOBAL_THIS);
  Object expression = globalThis.getPropertyAsObject(*runtime_, EXPRESSION);
  Function func = expression.getPropertyAsFunction(*runtime_, std::to_string(expressionIdx).c_str());
  return func.call(*runtime_, args);
}

Value JSIExecutor::callFunctionVec(const std::string& functionName, const std::vector<Value>& args) {
  Trace s("JSIExecutor::callFunctionVec - funcName: " + functionName);
  Object globalThis = globalObj_.getPropertyAsObject(*runtime_, GLOBAL_THIS);
  Function func = globalThis.getPropertyAsFunction(*runtime_, functionName.c_str());
  return func.call(*runtime_, args.data(), args.size());
}

Value JSIExecutor::callFunction(const std::string& functionName, const std::initializer_list<Value>& args) {
  Trace s("JSIExecutor::callFunction - funcName: " + functionName);
  Object globalThis = globalObj_.getPropertyAsObject(*runtime_, GLOBAL_THIS);
  Function func = globalThis.getPropertyAsFunction(*runtime_, functionName.c_str());
  return func.call(*runtime_, args);
}

Value JSIExecutor::callObjFunction(const std::string& objName, const std::string& functionName,
                                   const std::vector<Value>& args) {
  Trace s("JSIExecutor::callObjFunction - obj: " + objName + ", func: " + functionName);
  Object obj = globalObj_.getPropertyAsObject(*runtime_, objName.c_str());
  Function func = obj.getPropertyAsFunction(*runtime_, functionName.c_str());
  return func.call(*runtime_, args.data(), args.size());
}

Value JSIExecutor::getFunction(int expressionIdx) {
  Object globalThis = globalObj_.getPropertyAsObject(*runtime_, GLOBAL_THIS);
  Object expression = globalThis.getPropertyAsObject(*runtime_, EXPRESSION);
  Function func = expression.getPropertyAsFunction(*runtime_, std::to_string(expressionIdx).c_str());
  return Value(*runtime_, func);
}

void JSIExecutor::setGlobalVariable(const std::string& name, const std::string& json) {
  Trace s("JSIExecutor::setGlobalVariable - prop: " + name);
  Value val = Value::createFromJsonUtf8(*runtime_, reinterpret_cast<const uint8_t*>(json.c_str()), json.length());
  Object globalThis = globalObj_.getPropertyAsObject(*runtime_, GLOBAL_THIS);
  globalThis.setProperty(*runtime_, name.c_str(), val);
}

void JSIExecutor::setGlobalVariable(const std::string name, const std::string& propertyName, const std::string& json) {
  Trace s("JSIExecutor::setGlobalVariable - obj: " + name + ", prop: " + propertyName);
  Object globalThis = globalObj_.getPropertyAsObject(*runtime_, GLOBAL_THIS);

  auto setProperty = [this](Object& object, const std::string& propertyName, const std::string& json) {
    Value val = Value::createFromJsonUtf8(*runtime_, reinterpret_cast<const uint8_t*>(json.c_str()), json.length());
    object.setProperty(*runtime_, propertyName.c_str(), val);
  };

  if (globalThis.hasProperty(*runtime_, name.c_str())) {
    Object object = globalThis.getPropertyAsObject(*runtime_, name.c_str());
    setProperty(object, propertyName, json);
  } else {
    Object object(*runtime_);
    setProperty(object, propertyName, json);
    globalThis.setProperty(*runtime_, name.c_str(), object);
  }
}

void JSIExecutor::setGlobalHostFunc(const std::string& functionName, HostFunctionType funcType,
                                    unsigned int paramCount) {
  Trace s("JSIExecutor::setGlobalHostFunc - funcName: " + functionName);
  globalObj_.setProperty(
      *runtime_, functionName.c_str(),
      Function::createFromHostFunction(*runtime_, PropNameID::forUtf8(*runtime_, functionName), paramCount, funcType));
}

Value JSIExecutor::callNativeModule(const std::string& module, const std::string& method, const Array array) {
  Trace s("JSIExecutor::callNativeModule - module: " + module + ", method: " + method);
  if (!moduleRegistry_) {
    logError("module unregistered\n");
    return Value::undefined();
  }
  auto config = moduleRegistry_->getConfig(module);
  auto moduleID = config->moduleID;
  auto methodNames = config->methods;
  auto it = methodNames.find(method);

  if (it == methodNames.end()) {
    logError("invalid method\n");
    return Value::undefined();
  }
  auto syncMethodIds = config->syncMethodIds;
  std::vector<Dynamic> params;
  auto length = array.length(*runtime_);
  if (syncMethodIds.find(it->second) != syncMethodIds.end()) {
    // method type is sync, process the func and return immediately
    for (int i = 0; i <= length - 1; i++) {
      auto val = array.getValueAtIndex(*runtime_, i);
      auto dyn = dynamic::valueToDynamic(*runtime_, val);
      params.push_back(dyn);
    }
    // return val of the call will be pushed to args as the last val in the vector
    moduleRegistry_->callNativeMethodSync(moduleID, it->second, params);
    return dynamic::dynamicToValue(*runtime_, params[params.size() - 1]);
  }

  // method type is async, submit the native method call req to the queue and return undefined
  auto lastArg = array.getValueAtIndex(*runtime_, length - 1);

  // last arg should be the callback func
  assert(lastArg.isObject() && lastArg.getObject(*runtime_).isFunction(*runtime_));
  auto callbackId = callbackId_;

  callbacks_.insert({callbackId, lastArg.getObject(*runtime_).getFunction(*runtime_)});
  if (callbackId_ == std::numeric_limits<unsigned int>::max()) {
    logInfo(
        "alert: rolling over now, assuming callbackId 0 has been consumed, and consumers are actively "
        "consuming msg so that callbacks won't be override\n");
    callbackId_++;
  }

  Array newArgs(*runtime_, length - 1);  // exclude the last arg which is callback
  for (int i = 0; i <= length - 2; i++) {
    auto val = array.getValueAtIndex(*runtime_, i);
    auto dyn = dynamic::valueToDynamic(*runtime_, val);
    newArgs.setValueAtIndex(*runtime_, i, val);
    params.push_back(dyn);
  }
  callbackParams_.insert({callbackId, std::move(newArgs)});
  moduleRegistry_->callNativeMethodAsync(moduleID, it->second, params, callbackId);
  return Value::undefined();
}

Object JSIExecutor::getGlobalThis() { return globalObj_.getPropertyAsObject(*runtime_, GLOBAL_THIS); }

void JSIExecutor::invokeCallback(unsigned int callbackId) {
  Trace s("JSIExecutor::invokeCallback - callbackId: " + std::to_string(callbackId));
  auto callback = callbacks_.find(callbackId);
  auto callbackParams = callbackParams_.find(callbackId);
  if (callback == callbacks_.end() || callbackParams == callbackParams_.end()) {
    logError("invalid callback id\n");
    return;
  }

  callback->second.call(*runtime_, callbackParams->second);
  callbacks_.erase(callbackId);
  callbackParams_.erase(callbackId);
}
}  // namespace jsiexecutor
